package com.hmdp.service.impl;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.hmdp.Exception.ProjectNotExist;
import com.hmdp.domain.dto.Result;
import com.hmdp.domain.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

import static com.hmdp.content.MessageContent.NOT_EXIST_SHOP;
import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
@EnableConfigurationProperties
@Slf4j
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    ShopMapper shopMapper;
    ReentrantLock reentrantLock = new ReentrantLock();

    /**
     * 根据id查询商铺信息
     *
     * @param id
     * @return
     */
    @Override
    public Result queryById(Long id)  {
        //从redis中获取数据
        String shopJson = stringRedisTemplate.opsForValue().get(id.toString());
        Shop shop = null;
        if (shopJson != null) {
            //防止缓存穿透
            if ("".equals(shopJson)) {
                return Result.fail(NOT_EXIST_SHOP);
            }
            shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //防止缓存击穿设置互斥锁
        //redis中没有，在数据库中查找
        //获取互斥锁，设置锁的过期时间，防止死锁
        try {
            shop = updateUseLockCache(id, shop);
        } catch (ProjectNotExist | InterruptedException e) {
            return Result.fail(NOT_EXIST_SHOP);
        }

        return Result.ok(shop);
    }

    /**
     * 使用互斥锁的方式更新缓存
     * @param id
     * @param shop
     * @return
     * @throws InterruptedException
     * @throws ProjectNotExist
     */
    private Shop updateUseLockCache(Long id, Shop shop) throws InterruptedException, ProjectNotExist {
        while (shop == null) {
            if (reentrantLock.tryLock()) {
                //使用dcl保证只被缓存一次
                if (shop == null) {
                    try {
                        shop = getById(id);
                        //防止缓存穿透，将空值写入缓存，并设置过期时间
                        if (shop == null) {
                            stringRedisTemplate.opsForValue().set(id.toString(), "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                        }
                        //更新缓存
                        stringRedisTemplate.opsForValue().set(id.toString(), JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
                    } finally {
                        reentrantLock.unlock();
                    }
                }
            } else {
                //TODO 这里感觉还可以优化
                Thread.sleep(50);
            }
            shop = queryByIdFromRedis(id);
        }
        return shop;
    }

    /**
     * 保存店铺信息
     * 由于缓存穿透使用的空值策略，在保存数据时要注意缓存一致性
     *
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result saveShop(Shop shop) {
        // 写入数据库
        save(shop);
        //清理缓存，防止缓存穿透带来的数据不一致
        stringRedisTemplate.delete(shop.getId().toString());
        return Result.ok(shop.getId());
    }

    /**
     * 根据id更新商品店铺
     * 更新数据时保持数据一致性
     *
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result updateShop(Shop shop) {
        //更新数据库中的数据
        updateById(shop);
        //删除缓存中的数据
        stringRedisTemplate.delete(shop.getId().toString());
        return Result.ok(shop.getId());
    }



    /**
     * 从redis中获取
     *
     * @param id
     * @return
     */
    private Shop queryByIdFromRedis(Long id) throws ProjectNotExist {
        String shopJson = stringRedisTemplate.opsForValue().get(id.toString());
        if (shopJson != null ) {
            if (shopJson.equals("")){
                throw new ProjectNotExist(NOT_EXIST_SHOP);
            }
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        return null;
    }

    @Value("${hmdp.redis.comment-threshold}")
    String commentThreshold;
    @Value("${hmdp.redis.sold-threshold}")
    String soldThreshold;

    //缓存预热
    @PostConstruct
    public void cachedReservation() {
        QueryWrapper<Shop> wrapper = new QueryWrapper<Shop>().gt("sold", soldThreshold).or((i) -> {
            i.gt("comments", commentThreshold);
        });
        List<Shop> shopList = shopMapper.selectList(wrapper);
        for (Shop shop : shopList) {
            stringRedisTemplate.opsForValue().set(shop.getId().toString(), JSONUtil.toJsonStr(shop), RandomUtil.randomLong(CACHE_SHOP_TTL, CACHE_SHOP_TTL * 3),TimeUnit.SECONDS);
        }
        log.info("商铺数据预热完成");
    }
}
